﻿/*
 * Copyright 2011 Edward L. Stauff.                           Contact: <EdStauff@gmail.com>.
 * 
 * This file, "XerialScalarMappings.cs", is part of the "Stauffware.Common.Xml.Xerialization" 
 * library.  "Stauffware.Common.Xml.Xerialization" is free software: you can redistribute 
 * it and/or modify it under the terms of the GNU Lesser Public License as published by 
 * the Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common.Xml.Xerialization" is distributed in the hope that it will be 
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common.Xml.Xerialization".  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Xml;
using System.Text;
using System.Reflection;
using Stauffware.Common.Core;
using Stauffware.Common.Time;

namespace Stauffware.Common.Xml.Xerialization
{
	//#################################################################################
	/// <summary>
	/// Base type mapping class for types whose values can easily be expressed as a 
	/// single text string, e.g. integers, floats, booleans, enums, and strings.
	/// </summary>
	/// <remarks>
	/// When subclassing XerialScalarMapping, you usually only need to implement the
	/// FromXmlString method, and possibly override the ToXmlString methods.
	/// </remarks>

	public abstract class XerialScalarMapping : XerialTypeMapping
	{
		/// <inherit/>
		public override bool AllowsXmlElmnt { get { return true; } }
		/// <inherit/>
		public override bool AllowsXmlAttr { get { return true; } }
		/// <inherit/>
		public override bool AllowsXmlText { get { return true; } }

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="context">
		///		the context to which this mapping will belong (must not be null)
		///		</param>
		///	<param name="targetType">
		///		the type to be handled by this mapping (must not be null)
		///	</param>

		protected XerialScalarMapping (XerialContext context, Type targetType)
			: base(context, targetType, targetType.Name, XmlType.Attribute)
		{
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="context">
		///		the context to which this mapping will belong (must not be null)
		///		</param>
		///	<param name="targetType">
		///		the type to be handled by this mapping (must not be null)
		///	</param>
		/// <param name="xmlName">
		///		the default name to use for the XML element or attribute (must not be null)
		/// </param>

		protected XerialScalarMapping (XerialContext context, Type targetType, string xmlName)
			: base(context, targetType, xmlName, XmlType.Attribute)
		{
		}

		//-----------------------------------------------------------------------------
		/// <inherit/>

		public override void WriteToXmlElmnt (string xmlName, object targetObj, XmlWriter writer)
		{
			if (targetObj == null) return;
			if (xmlName == null) xmlName = this.XmlName;
			writer.WriteStartElement(xmlName);
			writer.WriteRaw(this.ToXmlString(targetObj));
			writer.WriteEndElement();
		}

		//-----------------------------------------------------------------------------
		/// <inherit/>

		public override void WriteToXmlAttr (string xmlName, object targetObj, XmlWriter writer)
		{
			if (targetObj == null) return;
			if (xmlName == null) xmlName = this.XmlName;
			writer.WriteAttributeString(xmlName, this.ToXmlString(targetObj));
		}

		//-----------------------------------------------------------------------------
		/// <inherit/>

		public override void WriteToXmlText (object targetObj, XmlWriter writer)
		{
			if (targetObj == null) return;
			writer.WriteRaw(this.ToXmlString(targetObj));
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads an object from one complete XML element (which may include attributes, 
		/// child elements, and/or text).
		/// </summary>
		/// <param name="reader">
		///		Where to read the XML from.
		///		On entry, the current node must be an Element.
		///		On exit, the current node will be the node immediately following the Element
		///			(for an empty Element) or the corresponding EndElement (for a non-empty element).
		/// </param>
		/// <returns>the newly created object</returns>

		public override object ReadFromXmlElmnt (NiceXmlReader reader)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXmlElmnt", reader);

			Require.Condition(reader.NodeType == XmlNodeType.Element);

		//	string str = reader.ReadInnerXml();		// not sure this is the right call to use
													// 15 Dec 2013 - It's not; it doesn't handle CDATA.
			string str = reader.ReadString();		// This does.
			reader.Skip();

			ParseResult result = this.FromXmlString(str);
			if (result.IsError)
				reader.BadXml(result.ErrorMessage);
			return result.Value;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads an object from one XML attribute.
		/// </summary>
		/// <param name="reader">
		///		Where to read the XML from.
		///		On entry, the current node must be either an Element or an Attribute.
		///		On exit, the current node will remain the same.
		/// </param>
		/// <param name="xmlName">the name of the XML attribute to be read</param>
		/// <returns>the newly created object</returns>

		public override object ReadFromXmlAttr (NiceXmlReader reader, string xmlName)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXmlAttr", reader);

			if (xmlName == null) xmlName = this.XmlName;

			ParseResult result;

			if (reader.NodeType == XmlNodeType.Element)
				result = this.FromXmlString(reader.GetAttribute(xmlName));
			else if (reader.NodeType == XmlNodeType.Attribute)
				result = this.FromXmlString(reader.Value);
			else throw new InputRequirement("XML reader must be on an Element or Attribute; found a " + 
												reader.NodeType.ToString());
			if (result.IsError)
				reader.BadXml(result.ErrorMessage);
			return result.Value;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads an object from XML text.
		/// </summary>
		/// <param name="reader">
		///		Where to read the XML from.
		///		On entry, the current node must be at the beginning of the text content to be read.
		///		On exit, the current node will be the first node not read.
		/// </param>
		/// <returns>the newly created object</returns>

		public override object ReadFromXmlText (NiceXmlReader reader)
		{
			string str = reader.ReadString();		// not sure this is the right call to use
			ParseResult result = this.FromXmlString(str);
			if (result.IsError)
				reader.BadXml(result.ErrorMessage);

			return result.Value;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Converts the given value to an XML string.
		/// This method is responsible for encoding characters appropriately for XML;
		/// e.g. "&amp;" -> "&amp;amp;".
		/// </summary>
		/// <param name="value">the value to be converted; will never be null</param>
		/// <returns>the value in XML string form</returns>

		protected virtual string ToXmlString (object value)
		{
			return value.ToString();
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Converts an XML string to a value.
		/// </summary>
		/// <param name="valueString">the XML string</param>
		/// <returns>a ParseResult containing the newly created value or an error message</returns>

		protected abstract ParseResult FromXmlString (string valueString);

		//-----------------------------------------------------------------------------
	}

	//#################################################################################
	/// <summary> Maps XML to a string. </summary>

	public class XerialStringMapping : XerialScalarMapping
	{
		/// <summary> constructor </summary>
		public XerialStringMapping (XerialContext context)
			: base(context, typeof(System.String)) { }

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			return new ParseResult(CharEncoding.EntitiesToUnicode(s), null);
		}
	}

	//#################################################################################
	/// <summary> Maps XML to a character. </summary>

	public class XerialCharMapping : XerialScalarMapping
	{
		/// <summary> constructor </summary>
		public XerialCharMapping (XerialContext context)
			: base(context, typeof(System.Char)) { }

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			s = CharEncoding.EntitiesToUnicode(s);
			if (s.Length != 1)
				return new ParseResult(null, "expecting exactly one character, not " +
							s.Length.ToString() + ": " + ErrorUtils.ValueToString(s));
			else return new ParseResult(s[0], null);
		}
	}

	//#################################################################################
	/// <summary> Maps XML to a boolean. </summary>

	public class XerialBooleanMapping : XerialScalarMapping
	{
		/// <summary> constructor </summary>
		public XerialBooleanMapping (XerialContext context)
			: base(context, typeof(System.Boolean)) { }

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			s = s.Trim().ToLower();
			if (s == "true" || s == "yes")
				return new ParseResult(true, null);
			else if (s == "false" || s == "no")
				return new ParseResult(false, null);
			else
				return new ParseResult(null, ErrorUtils.ValueToString(s) + 
					" is not a legal boolean value; expecting 'true', 'false', 'yes', or 'no'");
		}
	}

	//#################################################################################
	/// <summary> Maps XML to a scalar number of type T. </summary>

	public class XerialNumberMapping<T> : XerialScalarMapping
	{
		private delegate bool TryParseDelegate (string s, out T x);
		private TryParseDelegate TryParse;

		private static readonly Type[] tryParseArgs = { typeof(string), typeof(T).MakeByRefType() };

		/// <summary> constructor </summary>
		public XerialNumberMapping (XerialContext context)
			: base(context, typeof(T))
		{
			MethodInfo mi = this.TargetType.GetMethod("TryParse", tryParseArgs);

			Require.NotNull(mi, "no TryParse method for " + this.TargetType.FullName);
			Require.Condition(mi.IsStatic, "TryParse method is not static for " + this.TargetType.FullName);

			this.TryParse = (TryParseDelegate)Delegate.CreateDelegate(typeof(TryParseDelegate), mi);
		}

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			T value;
			if (this.TryParse(s, out value))
				return new ParseResult(value, null);
			else return new ParseResult(null, ErrorUtils.ValueToString(s) + 
												" is not a valid " + typeof(T).Name);
		}
	}

	//#################################################################################
	/// <summary> Maps XML to an enum type. </summary>

	public class XerialEnumMapping : XerialScalarMapping
	{
		private readonly bool IsFlags;

		/// <summary> constructor </summary>
		public XerialEnumMapping (XerialContext context, Type type)
			: base(context, type)
		{
			Require.Condition(type.IsEnum);
			object[] attrs = type.GetCustomAttributes(typeof(System.FlagsAttribute), false);
			this.IsFlags = (attrs != null && attrs.Length > 0);
		}

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			try
			{
				object value = Enum.Parse(this.TargetType, s, this.Context.IgnoreXmlCase);
				return new ParseResult(value, null);
			}
			catch (ArgumentException)
			{
				return new ParseResult(null, ErrorUtils.ValueToString(s) + 
									" is not a legal value for " + this.TargetType.Name);
			}
		}
	}

	//#################################################################################
	/// <summary> Maps XML to the enum type T. </summary>

	public class XerialEnumMapping<T> : XerialScalarMapping where T : struct
	{
		private readonly bool IsFlags;
		
		/// <summary> constructor </summary>
		public XerialEnumMapping (XerialContext context)
			: base(context, typeof(T))
		{
			Require.Condition(typeof(T).IsEnum);
			object[] attrs = typeof(T).GetCustomAttributes(typeof(System.FlagsAttribute), false);
			this.IsFlags = (attrs != null && attrs.Length > 0);
		}

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			T value;
			if (Enum.TryParse<T>(s, this.Context.IgnoreXmlCase, out value))
				return new ParseResult(value, null);
			else return new ParseResult(null, ErrorUtils.ValueToString(s) + 
									" is not a legal value for " + typeof(T).Name);
		}
	}

	//#################################################################################
	/// <summary> Maps XML to a DateTime. </summary>

	public class XerialDateTimeMapping : XerialScalarMapping 
	{
		private readonly DateTimeParser Parser;

		/// <summary> constructor </summary>
		public XerialDateTimeMapping (XerialContext context)
			: base(context, typeof(DateTime)) 
		{
			this.Parser = new DateTimeParser();
		}

		/// <inherit/>
		protected override string ToXmlString (object value)
		{
			return ((DateTime) value).ToString("");
		}

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			DateTimeParse dtp = this.Parser.Parse(s);
			if (dtp.GotValue)
				return new ParseResult(dtp.Value, null);
			else return new ParseResult(null, dtp.ErrorMessage);
		}
	}

	//#################################################################################
	/// <summary> Maps XML to a TimeSpan. </summary>

	public class XerialTimeSpanMapping : XerialScalarMapping
	{
		private readonly TimeSpanParser Parser;

		/// <summary> constructor </summary>
		public XerialTimeSpanMapping (XerialContext context)
			: base(context, typeof(TimeSpan))
		{
			this.Parser = new TimeSpanParser();
		}

		/// <inherit/>
		protected override string ToXmlString (object value)
		{
			return ((TimeSpan) value).ToString("");
		}

		/// <inherit/>
		protected override ParseResult FromXmlString (string s)
		{
			TimeSpanParse dtp = this.Parser.Parse(s);
			if (dtp.GotValue)
				return new ParseResult(dtp.Value, null);
			else return new ParseResult(null, dtp.ErrorMessage);
		}
	}

	//#################################################################################
	/// <summary>
	/// Encapsulates the result of a parsing attempt, whether successful or not.
	/// At least one of Value and ErrorMessage will always be null.
	/// </summary>

	public struct ParseResult
	{
		/// <summary>
		/// Gets whether the result is an error.  
		/// If true, Value will be null and ErrorMessage will be non-null.
		/// </summary>
		public bool IsError { get { return this.ErrorMessage != null; } }

		/// <summary>
		/// Gets whether the result has a value (that is, was successful).
		/// If true, Value will be non-null and ErrorMessage will be null.
		/// </summary>
		public bool HasValue { get { return this.Value != null; } }

		/// <summary>
		/// Gets whether the parse attempt found no value at all.
		/// If true, both Value and ErrorMessage will be null.
		/// </summary>
		public bool IsEmpty { get { return (this.Value == null) && (this.ErrorMessage == null); } }

		/// <summary>
		/// A message describing why the parse attempt failed.
		/// </summary>
		public readonly string ErrorMessage;

		/// <summary>
		/// The result of a successful parse attempt.
		/// </summary>
		public readonly object Value;

		/// <summary>
		/// Constructor.  At least one of value and errMsg must be null.
		/// </summary>
		/// <param name="value">The result of a successful parse attempt.</param>
		/// <param name="errMsg">A message describing why the parse attempt failed.</param>
		public ParseResult (object value, string errMsg)
		{
			Require.Condition((value == null) || (errMsg == null));
			this.Value = value;
			this.ErrorMessage = errMsg;
		}

	}

	//#################################################################################
}
